﻿// --- Copyright (c) 2006-2008 Stefan Kyntchev ---
// This software is written and copyrighted by Stefan Kyntchev 
// and BeyondPod Team members. All rights are reserved.
// Author contact: support@beyondpod.mobi
// ------------------------------------------------------------------------
// This file is part of BeyondPod RSS Feed Reader and Podcast manager
// (www.codeplex.com/BeyondPod) 
// BeyondPod is free software: you can redistribute it and/or modify 
// it under the terms of the GNU General Public License as published by 
// the Free Software Foundation, either version 3 of the License, or 
// (at your option) any later version. 
//  
// BeyondPod is distributed in the hope that it will be useful, 
// but WITHOUT ANY WARRANTY; without even the implied warranty of 
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
// GNU General Public License for more details. 
//  
// You should have received a copy of the GNU General Public License
// along with BeyondPod. If not, see <http://www.gnu.org/licenses/>
// ------------------------------------------------------------------------
// COPYRIGHT NOTICE: This copyright notice may NOT be removed, obscured or modified 
// without written consent from the author.
// --- End copyright notice --- 


using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using svs.Mobile.BeyondMedia.UiCore.GDI;

namespace svs.Mobile.BeyondMedia.UiCore.KineticControls
{
	public abstract partial class KineticListBase : Control
	{
		#region Data Memebers

		private const int SCROLL_THRESHOLD = 15;
		private const int SCROLL_TO_PERIOD = 300;
		private const int REFRESH_RATE = 11;
		private const double FRICTION_COEFF = 0.002F;
	
		private readonly Timer _ScrollTimer = new Timer();
		private readonly Timer _ScrollToTimer = new Timer();

		// Scrolling support
		protected bool _IsDragging = false;
		protected bool _IsScrolling = false;
		private bool _IsScrollingOnButtonDown = false;

		private int _CurrentScrollPosition;

		private int _LastMouseX;
		private int _LastMouseY;
		private int _MouseStartTime;
		private int _MouseEndTime;
		private double _ScrollVelocity = 0;
		protected int _MouseDownItemYPos = -1;

		// Scroll To
		private int _ScrollStartTime = 0;
		private int _ScrollStartPosition = 0;
		private int _ScrollChange = 0;
		private double _ScrollDuration = 0.0;

		private Point _MouseDownPosition = new Point(-1, -1);

		protected int _SelectedIndex = -2;

		private readonly Win32.WindowProcCallback _delegate;
		private readonly IntPtr _wndprocReal; // the original wndproc of

		private static Bitmap _offScreenBmp;
		private static Graphics _offScreenGraphics;
		protected  Rectangle _ContentRectangle = Rectangle.Empty;

		private int _ActiveListHeight = 0;
		private int _AverageItemHeight = 0;
		protected List<int> StartPositions = new List<int>();
		public readonly ListViewSettings Settings;

		#endregion

		public virtual string BackgroundHeaderText
		{
			get;
			set;
		}

		public virtual string BackgroundFooterText
		{
			get;
			set;
		}

		protected abstract int ItemCount
		{ 
			get;
		}

		protected int MaxScrollPosition
		{
			get
			{
				return _ActiveListHeight > _ContentRectangle.Height ? _ActiveListHeight - _ContentRectangle.Height : 0;
			}
		}

		protected static int MinScrollPosition
		{
			get
			{
				return 0;
			}
		}

		protected bool BackgroundHeaderVisible
		{
			get
			{
				return  _CurrentScrollPosition < MinScrollPosition;
			}
		}

		protected bool BackgroundFooterVisible
		{
			get
			{
				return _ActiveListHeight < Height || _CurrentScrollPosition > MaxScrollPosition;
			}
		}

		public KineticListBase()
		{
			InitializeComponent();

			_ScrollTimer.Tick += OnScrollTick;
			_ScrollToTimer.Tick += OnScrollToTick;

			_delegate = WnProc;
			_wndprocReal = Win32.SetWindowLong(Handle, Win32.GWL_WNDPROC,
			                                   Marshal.GetFunctionPointerForDelegate(_delegate));

			Settings = ListViewSettings.GetDefaultSettingsForPlatform(Gdi.GetScreenDpi(Handle));

			//Allow children to do any customizations
			OnCustomizeControlSettings(Settings);

			Settings.AdjustScreenValuesForCurrentDPI();
		}

		protected virtual void OnCustomizeControlSettings(ListViewSettings settings) {}

		private int WnProc(IntPtr hwnd, uint msg, uint wParam, IntPtr lParam)
		{
			bool handled = false;
			if (msg == Win32.WM_PAINT)
			{
				handled = OnPaint(hwnd);
			}

			return !handled ? Win32.CallWindowProc(_wndprocReal, hwnd, msg, wParam, lParam) : 0;
		}

		#region Drawing Functions


		#endregion
    
		#region Scrolling

		public void ScrollTo(int toPos)
		{
			ScrollTo(toPos, SCROLL_TO_PERIOD);
		}

		private void ScrollBar(int y)
		{
			_IsScrolling = true;
			_ScrollVelocity = 20;

			int maxScrolled = _ActiveListHeight <= _ContentRectangle.Height ? 0 : _ActiveListHeight - _ContentRectangle.Height;
			double pct = (double) (y - _ContentRectangle.Top)/(double) _ContentRectangle.Height;

			_CurrentScrollPosition = (int) (maxScrolled*pct);
			_CurrentScrollPosition = Math.Min(_CurrentScrollPosition, maxScrolled);
			_CurrentScrollPosition = Math.Max(_CurrentScrollPosition, 0);
		}

		public void ScrollTo(int position, int duration)
		{
			int minScrolled = 0;
			int maxScrolled = _ActiveListHeight <= _ContentRectangle.Height ? 0 : _ActiveListHeight - _ContentRectangle.Height;

			if (position < minScrolled)
				position = minScrolled;

			if (position > maxScrolled)
				position = maxScrolled;

			_ScrollStartPosition = _CurrentScrollPosition;
			_ScrollChange = position - _ScrollStartPosition;
			_ScrollDuration = duration;
			_ScrollStartTime = Environment.TickCount;

			_ScrollToTimer.Interval = REFRESH_RATE;
			_ScrollToTimer.Enabled = true;
		}

		protected void ScrollItemIntoView(int index)
		{
			Rectangle r = GetItemRectangle(index);
			if (r == Rectangle.Empty)
			{
				_CurrentScrollPosition = 0;
			}
			else if (_IsScrolling)
			{
				_CurrentScrollPosition = Math.Max(0, Math.Min(StartPositions[index], _ActiveListHeight - _ContentRectangle.Height));
			}
			else
			{
				if (r.Top < _CurrentScrollPosition)
					_CurrentScrollPosition = Math.Min(r.Top - index > 0 ? Settings.GroupPixHeight : 0, _ActiveListHeight - _ContentRectangle.Height);

				if (r.Bottom > _CurrentScrollPosition + _ContentRectangle.Height)
					_CurrentScrollPosition = Math.Max(0, r.Bottom - _ContentRectangle.Height);
			}

			RefreshList();
		}

		protected void ResetScrollPosition()
		{
			_CurrentScrollPosition = 0;
		}

		protected void ShowHeader()
		{
			_CurrentScrollPosition = ((2 * Settings.BackgroundHeaderPixMargin) + Settings.BackgroundHeaderPixHeight) * -1;
		}

		#endregion
    
		#region Control Event Handlers

		protected override void OnPaintBackground(PaintEventArgs e)
		{
		}

		protected bool OnPaint(IntPtr hWnd)
		{
			var ps = new Win32.PAINTSTRUCT();

			if(OffScreenGraphics == null)
				return false;

			var hdc = Win32.BeginPaint(hWnd, ref ps);
			var hdcMem = OffScreenGraphics.GetHdc();

			try
			{
				using (Gdi g = Gdi.FromHdc(hdc, ps.rcPaint))
				{
					using (Gdi gMem = Gdi.FromHdc(hdcMem, Rectangle.Empty))
					{
						Rectangle rect = ps.rcPaint;

						try
						{
							DrawScreenOn(gMem, rect, _CurrentScrollPosition);
						}
						catch (Exception)
						{
							throw;
						}

						g.BitBlt(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, gMem, rect.Left, rect.Top,
										 Win32.ERop.SRCCOPY);
					}
				}
			}
			finally
			{
				OffScreenGraphics.ReleaseHdc(hdcMem);
				Win32.EndPaint(hWnd, ref ps);
			}

			return true;
		}

		protected abstract void DrawScreenOn(Gdi mem, Rectangle rect, int position);

		public Bitmap CreateScreenShot(Size size)
		{
			Bitmap snapShotBmp = null;
			Graphics snapShotGraphics = null;
			var hdcMem = IntPtr.Zero;

			try
			{
				snapShotBmp = new Bitmap(size.Width, size.Height);
				snapShotGraphics = Graphics.FromImage(snapShotBmp);

				if(size != ClientSize)
					snapShotGraphics.Clear(Settings.ListBackgroundColor);

				hdcMem = snapShotGraphics.GetHdc();

				using (var gMem = Gdi.FromHdc(hdcMem, Rectangle.Empty))
				{
					DrawScreenOn(gMem, ClientRectangle, _CurrentScrollPosition);
				}
			}
			catch
			{
				if (snapShotBmp != null)
				{
					snapShotBmp.Dispose();
					snapShotBmp = null;
				}
			}
			finally
			{
				if (snapShotGraphics != null)
				{
					snapShotGraphics.ReleaseHdc(hdcMem);
					snapShotGraphics.Dispose();
				}
			}

			return snapShotBmp;
		}

		public Bitmap CreateScreenShot()
		{
			return CreateScreenShot(ClientSize);
		}

		protected override void OnMouseDown(MouseEventArgs e)
		{
			_LastMouseX = _MouseDownPosition.X = e.X;
			_LastMouseY = _MouseDownPosition.Y = e.Y;
			_MouseStartTime = Environment.TickCount;

			if (_LastMouseX > (_ContentRectangle.Right - Settings.SecondaryIconPixWidth) && !_IsScrolling && !_IsDragging)
			{
				_MouseDownItemYPos = _LastMouseY - _ContentRectangle.Top;
				RefreshList();
			}

			if (_IsScrolling)
			{
				_ScrollToTimer.Enabled = false;
				_ScrollTimer.Enabled = false;
				_ScrollVelocity = 0;
				_IsScrolling = false;
				_IsScrollingOnButtonDown = true;
			}
		}

		protected override void OnMouseUp(MouseEventArgs e)
		{
			var pt = new Point(e.X, e.Y);

			_MouseDownItemYPos = -1;

			if (_IsScrolling)
			{
				_IsScrolling = false;
				RefreshList();
				return;
			}

			// They scrolled the screen up too far
			if (_IsDragging && _CurrentScrollPosition < MinScrollPosition)
			{
				_IsDragging = false;
				ScrollTo(MinScrollPosition);
				SignalListEnd();
			}

				// They scrolled the screen down too far
			else if (_IsDragging && _CurrentScrollPosition > MaxScrollPosition)
			{
				_IsDragging = false;
				ScrollTo(MaxScrollPosition);
				SignalListEnd();
			}

				// now we're scrolling
			else if (_IsDragging)
			{
				_ScrollTimer.Interval = REFRESH_RATE;
				_ScrollTimer.Enabled = true;
				_IsScrolling = true;
				_IsDragging = false;
				return;
			}
			else
			{
				if (_IsScrollingOnButtonDown)
				{
					_IsScrollingOnButtonDown = false;
					RefreshList();
					return;
				}

				int pos = 0;
				if (pt.Y >= _ContentRectangle.Top)
				{
					pos = pt.Y + _CurrentScrollPosition - _ContentRectangle.Top;
					SelectItemInternal(GetPixelToItem(pos));

					Win32.InvalidateRect(Handle, ref _ContentRectangle, false);
					OnItemSelected(_SelectedIndex, pt);
				}
			}

			Win32.UpdateWindow(Handle);

			return;
		}

		protected override void OnMouseMove(MouseEventArgs e)
		{
			int x = e.X;
			int y = e.Y;
			int t = Environment.TickCount;

			if (_IsScrolling)
			{
				ScrollBar(y);
				RefreshList();
				return;
			}

			if (Math.Abs(y - _MouseDownPosition.Y) > SCROLL_THRESHOLD)
			{
				SelectItemInternal(-1);
				_IsDragging = true;
			}

			// SCROLL
			_CurrentScrollPosition = _CurrentScrollPosition - y + _LastMouseY;
			_LastMouseY = y;
			_LastMouseX = x;

			_ScrollVelocity = (double) (_LastMouseY - _MouseDownPosition.Y)/(t - _MouseStartTime);
			_MouseEndTime = t;

			RefreshList();

			return;
		}

		public static Graphics OffScreenGraphics
		{
			get
			{
				if (_offScreenGraphics == null)
					TryReAllocateOffsceenBuffer();

				return _offScreenGraphics;
			}
		}

		protected override void OnResize(EventArgs e)
		{
			_ContentRectangle = ClientRectangle;
			TryReAllocateOffsceenBuffer();
		}

		private static void TryReAllocateOffsceenBuffer()
		{
			if (_offScreenBmp != null &&
						_offScreenBmp.Width == Screen.PrimaryScreen.WorkingArea.Width &&
						_offScreenBmp.Height == Screen.PrimaryScreen.WorkingArea.Height)
				return;

			try
			{
				if(_offScreenBmp != null)
					ReleaseOffscreenBuffer();

				_offScreenBmp = new Bitmap(Screen.PrimaryScreen.WorkingArea.Width, Screen.PrimaryScreen.WorkingArea.Height);
				_offScreenGraphics = Graphics.FromImage(_offScreenBmp);
			}
			catch (OutOfMemoryException)
			{
				ReleaseOffscreenBuffer();
			}
		}

		private static void ReleaseOffscreenBuffer()
		{
			if (_offScreenGraphics != null)
			{
				_offScreenGraphics.Dispose();
				_offScreenGraphics = null;
			}

			if(_offScreenBmp != null)
			{
				_offScreenBmp.Dispose();
				_offScreenBmp = null;
			}
		}

		protected override void OnKeyDown(KeyEventArgs e)
		{
			if (ItemCount == 0)
				return;

			if (_IsScrolling)
			{
				_ScrollTimer.Enabled = false;
				_IsScrolling = false;
				_ScrollVelocity = 0;
			}

			if(_CurrentScrollPosition < 0)
			{
				e.Handled = true;
				ScrollTo(0);
				return;
			}

			switch (e.KeyCode)
			{
				case Keys.Up:
					SelectPreviousItem(GetPixelToItem(_CurrentScrollPosition + _ContentRectangle.Height - Settings.ListItemPixHeight), false);
					ScrollItemIntoView(_SelectedIndex);
					e.Handled = true;
					break;

				case Keys.Down:
					SelectNextItem(GetPixelToItem(_CurrentScrollPosition + Settings.ListItemPixHeight), false);
					ScrollItemIntoView(_SelectedIndex);
					e.Handled = true;
					break;
			}

			//Patch the * and # symbols
			if ((int)e.KeyData == 120)
				FindNextItemStartingWith('A');
			else if ((int)e.KeyData == 119)
				FindNextItemStartingWith('Z');

			base.OnKeyDown(e);
		}

		protected bool FindNextItemStartingWith(char keyChar)
		{
			if (ItemCount == 0)
				return false;

			int curr;

			if (_SelectedIndex > 0 && _SelectedIndex < ItemCount)
				curr = _SelectedIndex;
			else
				curr = 0;

			var start = curr;

			do
			{
				curr++;

				if (curr > ItemCount - 1)
					curr = 0;

				if (GetItemText(curr).StartsWith(keyChar.ToString(), StringComparison.InvariantCultureIgnoreCase)
						||
						GetItemText(curr).StartsWith(GetAlternativeKey(keyChar).ToString(),
																				 StringComparison.InvariantCultureIgnoreCase))
				{
					SelectItemInternal(curr);
					return true;
				}
			} while (curr != start);

			return false;
		}

		protected virtual string GetItemText(int ItemIndex)
		{
			return string.Empty;
		}

		protected override void OnKeyUp(KeyEventArgs e)
		{
			if(e.KeyCode == Keys.Enter)
				OnItemSelected(_SelectedIndex, Point.Empty);
		}

		protected override void OnKeyPress(KeyPressEventArgs e)
		{
			if (e.KeyChar == (Char)Keys.Escape)
			{
				e.Handled = true;
				OnBackKeyClicked();
			}

			FindNextItemStartingWith(e.KeyChar);
		}

		protected virtual void OnBackKeyClicked()
		{}

		#endregion

		#region Outgoing Event Notifications

		protected virtual void OnSelectionChanged(int index)
		{
		}

		protected virtual void OnItemSelected(int index, Point mousePosition)
		{
		}

		#endregion

		#region Helper Methods

		protected void SelectItemInternal(int index)
		{
			int old = _SelectedIndex;

			_SelectedIndex = index;

			if (old != _SelectedIndex)
			{
				if (_SelectedIndex >= 0)
				{
					if (IsItemVisible(_SelectedIndex))
						RefreshList();
					else
						ScrollItemIntoView(_SelectedIndex);
				}

				OnSelectionChanged(index);
			}
		}

		protected int GetPixelToItem(int y)
		{
			y = Math.Min((_ActiveListHeight - 1) - 1, y);
			y = Math.Max(0, y);

			int guess = y/_AverageItemHeight;
			int max = ItemCount;
			if (guess > max)
				guess = max;

			while (y < StartPositions[guess] && guess > 0)
			{
				guess--;
			}

			while (y >= StartPositions[guess + 1] && guess < max)
			{
				guess++;
			}

			return guess;
		}

		protected abstract bool IsItemNewGroup(int index);
		
		protected void CalculateItemHeights()
		{
			int c = 0;

			StartPositions.Clear();
			
			if (ItemCount == 0)
			{
				_ActiveListHeight = 0;
				_AverageItemHeight = Settings.ListItemPixHeight;
				return;
			}

			bool firstHeader = true;
			for (int i = 0; i < ItemCount; i++)
			{
				StartPositions.Add(c);

				c += Settings.ListItemPixHeight;

				if (!IsItemNewGroup(i)) continue;

				c += Settings.GroupPixHeight + Settings.MarginPixTop;

				if(!firstHeader)
					c+= Settings.MarginPixBottom;

				firstHeader = false;
			}

			StartPositions.Add(c); //The end of tha last item

			_ActiveListHeight = c + Settings.FooterPixHeight;
			_AverageItemHeight = _ActiveListHeight / ItemCount;
		}

		private void OnScrollTick(object sender, EventArgs e)
		{
			int t = Environment.TickCount;

			const int minScrolled = 0;
			int maxScrolled = _ActiveListHeight <= _ContentRectangle.Height ? 0 : _ActiveListHeight - _ContentRectangle.Height;

			// Time
			int dt = t - _MouseEndTime;

			// _ScrollVelocity
			if (_CurrentScrollPosition < minScrolled)
				_ScrollVelocity = (double) (_CurrentScrollPosition - minScrolled)/2/dt;
			else if (_CurrentScrollPosition > maxScrolled)
				_ScrollVelocity = (double) (_CurrentScrollPosition - maxScrolled)/2/dt;
			else
			{
				double dv = _ScrollVelocity*FRICTION_COEFF*dt;
				if (Math.Abs(dv) > Math.Abs(_ScrollVelocity))
					_ScrollVelocity = 0;
				else
					_ScrollVelocity = _ScrollVelocity - dv;
			}

			// Displacement
			double s = _ScrollVelocity*dt;
			if (s < 0 && s > -1 && _CurrentScrollPosition < minScrolled)
				s = -1;
			else if (s > 0 && s < 1 && _CurrentScrollPosition > maxScrolled)
				s = 1;

			// We're done scrolling
			if ((int) s == 0)
			{
				_ScrollTimer.Enabled = false;
				_IsScrolling = false;
				_ScrollVelocity = 0;
			}

			_CurrentScrollPosition = _CurrentScrollPosition - (int) s;
			_MouseEndTime = t;

			Win32.InvalidateRect(Handle, ref _ContentRectangle, false);
		}

		private void OnScrollToTick(object sender, EventArgs e)
		{
			double scrollTime = (Environment.TickCount - _ScrollStartTime);

			if (scrollTime < _ScrollDuration)
			{
				_IsScrolling = true;

				double dur = scrollTime/_ScrollDuration;
				dur -= 1;
				double multiplier = Math.Pow(dur, 3);
				multiplier += 1;
				double amount = _ScrollChange*multiplier;

				//_ScrollVelocity = (amount - _ScrollStartPosition)/_ScrollTime;
				_CurrentScrollPosition = _ScrollStartPosition + (int) amount;
			}
			else
			{
				_IsScrolling = false;
				//_ScrollVelocity = 0;
				_ScrollToTimer.Enabled = false;
				_CurrentScrollPosition = _ScrollChange + _ScrollStartPosition;
			}

			Win32.InvalidateRect(Handle, ref _ContentRectangle, false);
		}

		protected void RefreshList()
		{
			Win32.InvalidateRect(Handle, ref _ContentRectangle, false);
			Win32.UpdateWindow(Handle);
		}

		protected bool IsItemVisible(int index)
		{
			Rectangle r = GetItemRectangle(index);
			if (r == Rectangle.Empty)
				return false;

			return r.Top >= _CurrentScrollPosition && r.Top <= _CurrentScrollPosition + _ContentRectangle.Height ||
				     r.Bottom >= _CurrentScrollPosition && r.Bottom <= _CurrentScrollPosition + _ContentRectangle.Height;
		}

		Rectangle GetItemRectangle(int itemIndex)
		{
			if(itemIndex < 0 || itemIndex >= ItemCount)
				return Rectangle.Empty;

			return new Rectangle(_ContentRectangle.Left, StartPositions[itemIndex], _ContentRectangle.Width,
													 StartPositions[itemIndex + 1] - StartPositions[itemIndex]);
		}

		void SelectPreviousItem(int defaultIndex, bool byGroup)
		{
			int index = _SelectedIndex;

			if (index < 0)
			{
				index = defaultIndex;
			}
			else if (index == 0)
			{
			}
			else if (byGroup)
			{
				// Jump to the previous group
				index--;
				while (index > 0 &&
				       !IsItemNewGroup(index))
					index--;
			}
			else
			{
				index = Math.Max(0, index - 1);
			}

			SelectItemInternal(index);
		}

		void SelectNextItem(int defaultIndex, bool byGroup)
		{
			int index = _SelectedIndex;

			if (index < 0)
			{
				index = defaultIndex;
			}
			else if (byGroup)
			{
				// Jump to the next group
				int max = ItemCount - 1;

				if (index < max)
				{
					index++;
					while (index < max && !IsItemNewGroup(index))
						index++;
				}
			}
			else
			{
				index = Math.Min(ItemCount - 1, index + 1);
			}

			SelectItemInternal(index);
		}

		protected virtual void SignalListEnd() {}

		public static char GetAlternativeKey(char keyChar)
		{
			switch (keyChar)
			{
				case '1':
					return 'w';

				case '2':
					return 'e';

				case '3':
					return 'r';

				case '4':
					return 's';

				case '5':
					return 'd';

				case '6':
					return 'f';

				case '7':
					return 'x';

				case '8':
					return 'c';

				case '9':
					return 'v';

				default:
					return ' ';
			}
		}
    
		#endregion
	}
}